C#基础教程[3] 再谈类型
本文主要由zbx1425大佬所写
再谈数据类型
数据类型是一个初学者不太容易搞懂的概念,所以我们还要再强调一下。
首先,C#中的每一个“值”都是有类型的。包括由我们直接写出的称为“字面量”的东西(如 123, “Hello World”),变量的内容,以及函数返回的结果。(注:“返回 xxx” 意思就是函数运行的结果是xxx)
-
1
-int
: 不带小数点的数字是 int 类型的 “字面量” -
1.0
-double
: 带上小数点之后,它就成了一个 double 类型的 “字面量”
请仔细注意这里的区别。例如3 / 2
结果是 1,因为被除数和除数都是 int 所以执行了整数除法;但3.0 / 2
由于被除数是 double 会执行浮点数除法,结果就是 1.5 了,这就是类型不同引发的差异,即使是对于直接给出的“字面量”也是同样。
被除数和除数中只要有一个是浮点数就会执行浮点数除法
-
"1.0"
-string
: 双引号括起来的是 string 类型的 “字面量”,注意它和不带双引号的值1.0
的类型与意义可完全不同!以及,(与Python不同)单引号括起来的是另一个没提过的类型,C#
中区分单双引号,只能用双引号表示string
,不要用错。 -
Convert.ToInt32("3")
- 一个 string 传入Convert.ToInt32
函数,该函数返回 int 类型,所以它整体的结果是 int 类型的 3。
一个函数接收什么类型的值,与返回什么类型的值,是由其定义决定的。难道要一个个背?不用!SharpDevelop 提供了一个贴心小功能来让您快速了解这一关键信息。
在提示列表中查找函数时,或将鼠标指针放在代码中一个函数的名称上时,将会显示一个黄底的提示框,里面有形如这样的信息(此处以ToInt32
为例):
1 | [↑] 1 of 19 [↓] public static int ToInt32(string value); |
1 of 19
: 表示该函数有19种用法,也就是说可以接受19种参数(用专业名词说叫“重载”)(想不到这么多吧,233,这个函数挺强大的,不仅是string,一大堆东西都能给它转换成int),此处显示了第一种。可以点击左右的上下箭头按钮来查看全部的用法。public
static
: 这俩咱现在不用管。int
: 这个在ToInt32
函数名之前的第一个单词,就代表了它的返回类型。在这个例子中,您就知道这个函数(在这种用法中)运行之后的结果一定是一个int整数。(string value)
: 括号里的是使用这个函数时要传入的参数。前一个单词是类型,后一个单词是名字,有多个时会用逗号分开显示。您在上节课的练习中把Console.ReadLine()
读出来的string转换成int时,不就是在括号中传入了一个string类型的值-输入进来的文字么?
我们再来看一看下面这段代码。
1 | Console.WriteLine(Convert.ToString(Convert.ToDouble(Console.ReadLine()) / 2)); |
这段代码看起来很长,对于初学可能相对难以理解,让我们一点一点来解释。
我们上次讲过嵌套函数的运行类似数学中的复合函数 , 从最内层的函数开始运行, 然后把返回的数值代入到外面一层的函数中作为参数。
- 首先是最内层的
Console.ReadLine()
运行,并把读入的一行字以 string 类型返回。如果输入的是 5,现在代码就变成了:
1 | Console.WriteLine(Convert.ToString(Convert.ToDouble("5") / 2)); |
- Convert.ToDouble(“5”) 运行,将 “5” 转换成 double 的 5.0 并返回。
1 | Console.WriteLine(Convert.ToString(5.0 / 2)); |
- 由于一边是 double 一边是 int,运行了浮点数除法,结果是 double 的 2.5。随后 2.5 被Convert.ToString 转换成 string 的 “2.5”,然后被 Console.WriteLine 写到窗口中。
- 其实在这个例子中,如果去掉 Convert.ToString,也照样能够输出。这是因为 Console.WriteLine 也是有一大堆“用法”,不仅接受 string 也接受 double,您不妨在那个“用法列表”里找一找。
显然,如果不把值放在一个变量里,那一行代码执行完了之后值就没了。变量就是一种储存值的容器。正如上一节课中介绍的,在C#中,一个变量只能储存一种特定类型的值。
比如我们需要以 double
类型保存下来输入的内容以备以后再用, 那就把上面的代码改成:
1 | double a = Convert.ToDouble(Console.ReadLine()); |
new 运算符
编程中要处理各种各样的数据, 要是只有我们学过的 int
string
double
那几种类型可能会十分麻烦, 例如要是用 int
来保存当前时间的话 就需要 6 个 int
类型的变量来分别保存年月日时分秒。因此 C#
里除了我们已经用到的 string int double 之外,还提供了很多其他的类型。
那么问题来了。在使用 string int double 的时候,除了从函数中取得(如Console.ReadLine()
),我们想要用一个值的时候都可以用字面量的形式写出来。比如我们想要整数1就可以直接写1,想要字符串Hello就直接"Hello"。但是字面量就只能表示这些最基础的类型了。
假如我们想要一个表示 1919年8月10日11:45:14 的时间(在C#中有个专门的类型用来表示和处理时间,叫DateTime
),就没有办法用字面量写出来了,怎么办呢?
大佬们自然给我们想好了。这就是"new 运算符"。new DateTime(1919, 8, 10, 11, 45, 14)
就会给我们一个我们想要的值了。
1 | DateTime a = new DateTime(1919, ...); // 既可以存在变量里(此处省略后面的数字) |
这个用法是不是很像在用一个函数?实际上它就是一个特殊的函数(叫“构造函数”),必须要前面配着"new"来用,而且会给你一个该类型的值。和函数一样,你也可以使用那个小黄框来看它的各种用法。
不过为什么要用这些别的类型呢?因为微软在这些类型内部封装了一些便利的功能,来帮助我们完成一些与它们有关的操作。专事专用,用日期专用的类型来处理日期就会方便很多。例如,如果我们要算两个日期之间差几天,如果你用 int 来表示每个的年月日,自己来算日期,就得自己处理进位借位(进到下个月,退到上个月)、每个月多少天、是不是闰年等麻烦事。但是微软的大佬们已经给我们预先准备好了日期减日期的运算程序。我们可以直接用:
1 | DateTime date1 = new DateTime(2021, 12, 5, 0, 0, 0); // 2021-12-5 |
当然,不是所有问题都有提前做好的解决程序(不然咱还学编程干嘛),不过要是有为啥不用呢?多方便。
成员
接下来我们深入地讲一下上一节课讲到的成员访问(“.”)。
为了简便(以及与专有名词接轨)起见,我们从现在开始将某一个类型的值称为“对象”。对象这个词中文上可能相对不太好理解, 对象的英文是 object
, 作为编程术语时翻译为对象, 它也有 “宾语”, "物体"的意思, 换句话说, 我们生活中能见到的非抽象的东西都可以叫对象。
如,上面这个例子中,date1
变量里储存了一个 DateTime
类型的对象。把它理解成“一个东西”即可。
成员可以理解为是一个对象“里面的”内容,可以使用"."来获得或使用。例如:Year
Month
Day
都是 DateTime
的成员。在上面一个例子中,date1.Year
就会返回 int
类型的 2021,它是这个日期的“年”成份。
有些成员是“非静态”的。别被这个不知所云的名词吓倒,它的意思是,这个内容是和一个特定的对象相关的,而不是和这个类型的总体泛泛地相关的。也就是说,非静态的不是对于每个对象都一样的。而“静态”(就是我们之前看到的 static
修饰符)与之正相反,是一种总体的东西,与整个类型有关,和每个单独的对象没有关系。
好像有些不太好理解,我们举个例子。例如,刚才提到的Year
就是DateTime
的非静态成员,因为不同的日期的值的年份可能不同。或者再举个例子,如果我们有一个“学生”类型,那么“班级”就是它的非静态成员,因为不同的学生可能在不同的班;但是“腿数”就可能是个静态成员,因为所有学生都是两条腿——当然在设计的时候把它做成非静态的也行,不过不太有必要。
如果要用一个非静态的成员,在一个值的后面加".“就可以访问到它了。例如上面的 date1.Year
就是获取了date1 这个对象 的 Year 成员。如果要用一个静态的成员,在类型名的后面加”."即可。如 int.MaxValue
就获取到了 int
类型所能表示的最大数值。您也想必很容易理解,“能表示的最大数值”这种东西一般是“静态”的。
您甚至可以套好几层的成员访问。用上面那个学生的例子就是:“小明.班.学生人数” 获得小明这个学生所在的班的学生人数。
成员不仅可以是值,还可以是函数。例如我们一直在用的 Console.WriteLine
就是 Console
类型的一个静态成员函数(您是直接用"Console"这个类型名,而不是在 new 一个 Console
对象来用的)。
您可能会说:不对呀,输出内容不是和特定的窗口相关的吗,为什么它是静态的呢?这是因为Windows系统规定每个程序只能有一个控制台窗口,所以既然只有一个为了方便起见就干脆搞成静态的了。
您应该已经感受到值和函数的区别还是挺大的,值可以直接用,但是函数就得加()
来调用,有时候还得在括号里写参数。如何区分呢?
您可能已经注意到,当您打出一个"."字时,SharpDevelop将自动弹出提示列表,里面就会列出它所有成员的名字。每个名字的左侧都有一个图标,形如“蓝色方块”和“手拿着表格”的分别是“成员变量”和“属性”,不用太计较它们的区别,它们都可以直接当作一个值来用,例如刚才的date1.Year
。形如“紫色方块”的就是函数,它们要用括号来调用,例如Console.WriteLine(...)
。
善用自动提示!SharpDevelop的提示功能与小黄框可以显示各个成员的名称、类型和用法,您几乎不用背诵任何内容。
什么是 “null”?
在开始讲这一节之前,我们先来引入一个有趣的东西:随机数发生器。
这同样也是个类型——Random
,一个 Random
的对象有一个叫 Next
的非静态成员函数,使用时需提供两个 int 类型参数分别表示最小和最大值(左闭右开),反复调用即可从里面不断取出不同的数。用来做猜数游戏想必很有趣。还是那句话,只是为了举例子,不用背。
假设我们想得到一个1~50的随机数,写了这样的代码:
1 | Random rand; |
运行一下,报错了!出现了"NullReferenceException"(空引用错误)。这是为什么呢?
原来,Random rand
这样声明变量,只是创建了一个叫 rand 的,可以存储 Random 对象的“盒子”。rand的里面是空的,并没有把一个实际的 Random 随机数生成器对象“装”到里面。
当我们在 rand.Next
的时候,我们要把这个生成器从"rand"这个"盒子"拿出来让它给我们生成个随机数--但rand里现在是空的,并没有一个生成器被装在里面。所以自然出现了问题。
那我们应该怎么做呢?我们要用某种方法获得一个生成器的对象(值),然后把它装到 rand 这个“盒子”里。怎么获得呢?还记得 new 运算符么?
1 | Random rand; |
接下来介绍"null"。"null"是个特殊的值,表示“没有值(没有对象)”的这么一个状态。例如,最开始的时候,rand里没有对象(没有值),也可以说“rand的值是null”。
你还可以给变量赋值为null。这代表着把里面已经有的对象(如果有)给扔掉。
1 | Random rand = new Random(); // 搞出来一个生成器搁进去 |
类似的,如果想知道一个变量里面有没有正在存着一个值,可以使用 rand == null
和rand != null
判断。
特殊的是,内置的 int 和 double (以及部分别的,如DateTime)是“值类型”。与刚刚的 random 的区别在于,一个值类型的变量(如int a
)一被创建就会自动地装有一个对象(一般会是0)。可以把它修改成别的值(a = ...
),但是不能扔掉(a = null
在编译时就会报错)。所以您永远都不会担心自己 int 类型的变量会是 null 。
相反的,大多数其他类型就都是可以为"null"的了(这种类型的学名叫“引用类型”),所以用的时候一定要注意。如果您不太拿得准,最好在声明变量的时候就用"new"搞个值出来赋给它。
string 是个有趣的例子,因为它也是个“引用类型”。也就是说,“” 是个空字符串, 而 null 则是“没有字符串”。
关于这个编程上有一个相关的非常形象的笑话:卫生间里挂厕纸的那个挂钩,字符串 "abc"
是挂钩上挂着一卷纸;""
是挂钩上没纸了,但是还挂着那个硬质壳的芯;null
则是干脆钩上啥也没了。
这也是要注意的一点。当访问 string 的成员时(string 有个叫 Length 的非静态属性,代表它的长度):
1 | string a = "abc"; Console.WriteLine(a.Length); // 输出 3 |
因为c是null,所以试图访问c.Length
会直接导致"NullReferenceException"错误。
当然,日常使用中我们不太会经常遇到"null"的string,所以不用过于担心;但您还是需要了解这种现象。